Skip to content

Harden SwiftData store against data loss (demo wipe + schema versioning)#2

Open
hoveeman wants to merge 11 commits into
feature/coach-apple-on-devicefrom
fix/demo-data-safety
Open

Harden SwiftData store against data loss (demo wipe + schema versioning)#2
hoveeman wants to merge 11 commits into
feature/coach-apple-on-devicefrom
fix/demo-data-safety

Conversation

@hoveeman

@hoveeman hoveeman commented Jun 29, 2026

Copy link
Copy Markdown
Owner

Why

A TestFlight tester (the app owner) lost real data, repeatedly replaced by the upstream demo seed (Saksham / Pittsburgh workouts). Root cause was not an automatic seed in production — it traced to running a -seedDemo dev build against the same device/bundle id that holds real data (which wrote demo rows into the persistent store), plus the all-or-nothing "Clear demo data" button. Because TestFlight builds are archived from an uncommitted working tree, every build's safety depended on that tree's state.

This makes data loss structurally impossible in Release builds, and adds explicit schema versioning so future model changes migrate safely instead of risking the store.

Changes

Close the wipe paths (Release-safe)

  • RootViews — launch-time clearAll() + seedDemo() was gated only by a seedDemo UserDefaults bool, so it could run in any configuration. Now #if DEBUG. Archived builds can no longer wipe the store from a stray default.
  • PrivacyDataSettingsView — "Clear demo data" / "Reseed demo data" both call SeedData.clearAll() (deletes every row of every model). Gated behind #if DEBUG, removing the only in-app full-wipe path from shipping UI.

Explicit schema versioning + safe failure

  • PulseLoopSchemaV1 baselines the 23 current @Model types as version 1.0.0. Existing on-disk stores match this shape exactly, so they're recognised as already-at-V1 — no migration runs, no data is touched.
  • PulseLoopMigrationPlan lists V1 with no stages yet; doc comments spell out the per-change procedure for introducing V2 (+ a lightweight or custom stage). This replaces SwiftData's implicit inference, which fails silently on non-additive changes.
  • ModelContainerFactory builds against the versioned schema and passes the plan. On an unexpected/unmigratable store it moves the store (and -wal/-shm sidecars) to a timestamped backup and recreates — never fatalError crash-loop, never silent data drop.

How to add the next schema version

When you change a model, follow the steps in the PulseLoopSchemaV1 doc comment: add PulseLoopSchemaV2, append a MigrationStage (.lightweight for additive/renames, .custom for transforms), and point currentSchema at V2. Purely additive changes (new model, new optional property) need only the new version + a .lightweight stage.

Testing

  • xcodebuild buildDebug and Release both succeed (signing-free simulator).
  • xcodebuild testall 168 unit tests pass; they create the container in-memory through ModelContainerFactory, exercising the migration plan at runtime.

Note

The separate background crash (MainActor.assumeIsolated in the BGTask handler) was already fixed earlier on this branch (a0456f3) and is not part of this PR.

🤖 Generated with Claude Code

hoveeman and others added 5 commits June 28, 2026 17:16
…cations

Adds an "On-device (Apple)" provider that runs the AI coach entirely on the
device via FoundationModels — no API key, no network, fully private. It falls
back to a chosen cloud provider (OpenAI/Gemini/OpenRouter) when the local model
is unavailable or a generation fails.

Provider:
- AppleFoundationModelsClient: a ResponsesClient adapter (one-shot, tool-less in
  v1) using FoundationModels guided generation for structured cards, with a
  text/JSON fallback. Mirrors the GeminiClient request-body translation.
- AppleOnDeviceAvailability: friendly wrapper over SystemLanguageModel
  availability; guarded with #if canImport(FoundationModels) / @available so the
  project still compiles on older SDKs.
- FallbackResponsesClient + CoachClientResolver: unify provider resolution
  across chat/summaries/notifications and add the on-device → cloud-backup chain.
- Settings: On-device provider option, cloud-backup picker, privacy card.

Notifications (leveraging free/private local inference):
- Richer check-ins: actionable tip + tappable follow-up + adaptive skip.
- New midday slot alongside morning/evening.
- Proactive anomaly alerts (low SpO2, short sleep), event-driven and
  on-device-only, deduped per kind per day.
- On-device check-ins use a BGProcessingTask (longer budget, no network
  required); cloud providers keep BGAppRefreshTask. Adds the
  com.pulseloop.coach.process background-task identifier.
- Fixes a latent crash: background scheduling now gates on actual BGTask
  registration (the host app could otherwise submit an unregistered identifier
  under XCTest).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three independent ways a build could destroy real user data are removed
or made recoverable:

- RootViews launch-time `clearAll()`+`seedDemo()` was gated only by a
  `seedDemo` UserDefaults bool, so it ran in any configuration. Wrap it
  in `#if DEBUG` — archived Release/TestFlight builds can no longer wipe
  the store from a stray default.
- The Privacy & Data "Clear demo data" / "Reseed demo data" buttons both
  call `clearAll()` (deletes every row of every model). Gate them behind
  `#if DEBUG` so the only in-app full-wipe path is gone from shipping UI.
- ModelContainerFactory replaced its destructive failure mode: on a
  failed inferred migration it now moves the existing store (and -wal/-shm
  sidecars) to a timestamped backup and recreates, instead of crash-looping
  or silently dropping data. Additive schema changes still migrate
  automatically; non-additive ones preserve the old store for recovery.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adopt VersionedSchema/SchemaMigrationPlan so schema changes between
TestFlight builds run explicit, reviewable migrations instead of
SwiftData's implicit inference (which fails silently on non-additive
changes and risks wiping real user data).

- PulseLoopSchemaV1 baselines the 23 current @model types as version
  1.0.0. Existing on-disk stores match this shape exactly, so they're
  recognised as already-at-V1 — no migration runs, no data is touched.
- PulseLoopMigrationPlan lists V1 with no stages yet; doc comments spell
  out the per-change procedure for adding V2 (+ lightweight/custom stage).
- ModelContainerFactory builds against the versioned schema and passes
  the plan. The backup-then-recreate fallback remains as the last-resort
  net for an unexpected/unmigratable store.

Verified: Debug + Release simulator builds succeed; all 168 unit tests
pass (they create the container in-memory through this factory).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@hoveeman hoveeman changed the title Prevent demo seed from wiping real user data Harden SwiftData store against data loss (demo wipe + schema versioning) Jun 29, 2026
saksham2001 and others added 6 commits June 29, 2026 12:09
…imodal-images

Add image input to the AI Coach (multimodal)
…-for-upstream

Add Apple on-device (FoundationModels) coach provider + rich notifications
The background-task guard in flushNow() (commit ed608a5) references
UIApplication, but its matching import lived only in uncommitted local
work. Restore the canImport(UIKit) guard so the branch compiles standalone.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants